#!/bin/bash
# Version: 0.1.2
#  Script that uses the Solaris Package Companion script multiple times to
#  find the minimum metacluster, cluster, and package level required for 
#  a package you want to check.  This presumes that you have already created 
#  a repository for SPC to use.
# 
#  You can specify the package from the command line call, and even the base level
#  you want to test against to show your upgrade path.  If you do not specify a
#  package, it will prompt you.  If you do not specify an install level to base against
#  it assumes SUNWSmreq (the least you could have installed to have a functioning
#  system and shows you what packages you need, what meta-cluster / cluster you need
#  to install to get there.  I feel like I'm reimplementing a craptacular version
#  of a lobotomized dpkg utility without the ability to actually install anything
#  but hey, Solaris didn't already have this feature so here is my attempt.
#
# TODO list 
#  - right now it depends on using SPC with a repository (like the Product dir on the DVD)
#    + SPC is capable of using the installed system.  this script should be too with some tweaks
#    + I could probably create the SPC call in the beginning as a var and modify the uses of pkginfo
#    + to have or not have the -d switch depending on which mode we're operating in.  This will
#    + affect the SUNWblah.i packages as once they are installed, for pkginfo, you do not refer to 
#    + them with the .i appendation whereas drawing from a repository like the DVD, you do.
#  - create graphviz export of dependencies
#  - Write routine for creating a first repository for SPC to use so that we can use this wrapper
#  - Create a SV4 package for this
#

# redefinable constants for your environ - these will need to be set
SPC=/path/to/spc-v0.9.ksh
SPC_REP=/path/to/.spc_repository
PKG_REP=/path/to/Solaris_10/Product
TMPDIR=/var/tmp/
BASE_NUM=1
SFW_REP=/path/to/Solaris_Software_Companion/Solaris_i386/Packages 

# do not change these
flags[0]=SUNWCmreq # not a "software group" you can choose in install menu
flags[1]=SUNWCrnet # Reduced Network Support Software Group 2.0 GB
flags[2]=SUNWCreq  # Core System Support Software Group  2.0 GB
flags[3]=SUNWCuser # End User Solaris Software Group  5.3 GB
flags[4]=SUNWCprog # Developer Solaris Software Group 6.6 GB
flags[5]=SUNWCall  # Entire Solaris Software Group  6.7 GB
flags[6]=SUNWCXall # Entire Solaris Software Group Plus OEM Support  6.8 GB
declare -i recurse_lev
recurse_lev=0
myPID=${$}
declare -a pkg_dep_tmp
dne_error="repository"
spaces="" # for fancy dancy verbose output of deps
myName=$0
listmode=0
verbose=0
scriptSafe=0
declare listFile
declare seedFile
NO_ARGS=0 
E_OPTERROR=65
package=""
bAnchor="^"
eAnchor="$"

#  Function: 
#     getdeps ()
#  Purpose:
#     Does the work of querrying the dependencies of a package
#     based on a base install level, recursively queries for unmet dependencies 
#     and constructs a list of all the packages that need to be installed to satisfy
#     those unmet dependencies.  This is not as craty as apt-get as it does only rudimentary
#     intelligence in ordering those packages, but it should suffice.
#  Parameters:
#     $1   the name of the package that we want to query the deps of
#  Expected Result:
#     - Files altered: 
#     $listfile and $checked are potentially altered to build the dependency list.
#     All of the dependencies for the package, and the package are added to the list if $1 
#     is not yet installed with the Software Group base specified.  Otherwise the list is unchanged.
#     unmet dependendencies for will be added to the list followed by $1 so that they can be
#     installed in that order and this should work.
#     - Depending on verbosity, messages are output to stdout
#  Return conditions:
#     1 for false.
#      - If the name of the package passed in does not exist in the repository or installed system
#      - If the parameter passed in is zero length
#     0 for true
#      - if the package is already in the list
#      - if the package has already beed checked on a previous recursion
#      - if the package is already in the Base Install Software Group (default or specified)
#      - if all the deps for the package and the package are able to be added to the list
# 
getdeps()
{
  if [ -z "$1" ]
  then
    echo "${spaces}ERROR - getdeps parameter is zero length FAIL"
    return 1
  fi

  if [[ $verbose -ge 1 ]]; then echo; echo "${spaces}$1 - Getting Dependencies"; fi

  if /usr/xpg4/bin/grep -q "^${1}$" $checked
  then
    if [[ $verbose -ge 1 ]]; then echo "${spaces}'-- Pruning - package $1 already checked"; fi
    return
  fi

  if /usr/xpg4/bin/grep -q "^${1}$" $pkg_dep_unmet
  then
    if [[ $verbose -ge 1 ]]; then echo "${spaces}'-- Pruning - package $1 previously added to dependency list"; fi
    return
  fi

  isSFW $1; SFWness=$?
  # set up dep count for SFW packages 
  if [[ $SFWness -eq 0 ]]
  then
    if [ -e ${SFW_REP}/${1}/install/depend ]
    then
      no_of_deps=$(cat ${SFW_REP}/${1}/install/depend | grep "^P " | wc -l)
    else
      no_of_deps=0
    fi
    if [[ $verbose -ge 1 ]]; then echo "${spaces}'-- In Solaris Software Companion Collection"; fi    
    if [[ $verbose -eq 1 ]]; then echo; echo "$(pkginfo -d $SFW_REP $1 2>/dev/null | sed "s/^/${spaces}/")"; fi
    if [[ $verbose -eq 2 ]]; then echo; echo "$(pkginfo -xd $SFW_REP $1 2>/dev/null | sed "s/^/${spaces}/")"; fi
    if [[ $verbose -eq 3 ]]; then echo; echo "$(pkginfo -ld $SFW_REP $1 2>/dev/null | sed "s/^/${spaces}/")"; fi

  # set up dep count for standard dist packges and prune if in Soft Group
  else
    # If a package name is wrong, or is the name of a package not in the repository, they do not have
    # information so SPC returns a statement about not being in this repository.
    if echo $($SPC -r $SPC_REP -d $1 | grep "repository") | /usr/xpg4/bin/grep -q "repository"
    then
      if [[ $verbose -ge 5 ]]; then echo "${spaces}Package name error or package missing from repository: $1, skipping"; fi
      rm ${pkg_dep_tmp[$recurse_lev]} # done with temp file
      return 1    
    fi

    # verbose mode output the meta-cluster and cluster this package comes in
    metacount=$($SPC -r $SPC_REP -P $1 | wc -l)
    if [[ $verbose -ge 5 ]]; then echo; echo "${spaces}metacount: $metacount"; fi

    if [[ $verbose -ge 1 ]]; then  echo "${spaces}'-- in software group: ---------> ${flags[7-$metacount]}"; fi
    toEcho=$($SPC  -r $SPC_REP -p $1)
    if [[ $verbose -ge 1 ]]; then echo "${spaces}'-- inside of cluster: ---------> $toEcho"; fi
    # there are some arch specific packages that have a .i appended to the package dir name
    # e.g. SUNWcar is SUNWcar.i in x86 install sets for some reason...
    # these need special handling for pkginfo to get their information properly. 
    if [[ -e ${PKG_REP}/${1}.i ]] && [[ $verbose -ge 1 ]]
    then
      if [[ $verbose -eq 2 ]]; then echo "$(pkginfo  -d $PKG_REP ${1}.i 2>/dev/null | sed "s/^/${spaces}/")"; fi
      if [[ $verbose -eq 3 ]]; then echo "$(pkginfo -xd $PKG_REP ${1}.i 2>/dev/null | sed "s/^/${spaces}/")"; fi
      if [[ $verbose -ge 4 ]]; then echo "$(pkginfo -ld $PKG_REP ${1}.i 2>/dev/null | sed "s/^/${spaces}/")"; fi
    else    
      if [[ $verbose -eq 2 ]]; then echo "$(pkginfo  -d $PKG_REP $1 2>/dev/null | sed "s/^/${spaces}/")"; fi
      if [[ $verbose -eq 3 ]]; then echo "$(pkginfo -xd $PKG_REP $1 2>/dev/null | sed "s/^/${spaces}/")"; fi
      if [[ $verbose -ge 4 ]]; then echo "$(pkginfo -ld $PKG_REP $1 2>/dev/null | sed "s/^/${spaces}/")"; fi
    fi
    # if its in the base Soft Group, skip. Any package in a given SG is also in large SGs
    # so count how many SGs are returned by SPC
    if [[ 7-$metacount -le $BASE_NUM ]] 
    then
      if [[ $verbose -ge 1 ]]; then echo "${spaces}'-- Already installed with -----> ${flags[$BASE_NUM]}"; fi
      if [[ $verbose -ge 5 ]]; then echo "${spaces}'-- Adding to list of already checked packages"; fi
      echo "$1" >>$checked
      return 0
    else
      no_of_deps=$($SPC -r $SPC_REP -d $1 | wc -l | sed 's/ //g')
    fi 
  fi

  # if no deps, skip to after the loop
  if [[ $no_of_deps -eq 0 ]]
  then
    if [[ $verbose -ge 1 ]]; then echo "${spaces}'-- no deps.  Donce checking deps for $1"; fi
  else 
    # Get the deps and store in temp file
    pkg_dep_tmp[$recurse_lev]=`mktemp ${TMPDIR}${myname}.${myPID}.${recurse_lev}.pkg_dep_tmp.XXXXXXX`
    if [[ $SFWness -eq 0 ]]
    then # do it the SFW way
      awk '/^P/ {print $2}' ${SFW_REP}/${1}/install/depend >${pkg_dep_tmp[$recurse_lev]}
    else
      $SPC -r $SPC_REP -d $1 >${pkg_dep_tmp[$recurse_lev]}
    fi
    
    # Verbose header
    if [[ $verbose -ge 1 ]]; then echo; echo "${spaces}$1 depends on:"; fi
      # add padding to $spaces to indent the output of the recursive call
    spaces="${spaces}     "
    if [[ $verbose -ge 2 ]]; then echo "${spaces}--( ${no_of_deps} dependencies found )--";echo; fi
    if [[ $verbose -ge 5 ]]; then echo "${spaces}contents of ${pkg_dep_tmp[$recurse_lev]}"; echo "$(cat ${pkg_dep_tmp[$recurse_lev]} | sed "s/^/${spaces}/")";echo; fi

    # I discovered a circular reverse dependency in SUNWdhcsr and SUNWdhcsu that
    #  was causing infinite loops so this checks for in the list of dependenies
    #  for depending package and removes it from the list
    if [[ $verbose -ge 5 ]]; then echo "${spaces}'-- Adding to list of already checked packages"; fi
    echo "$1" >>$checked

    if [[ $verbose -ge 5 ]]; then echo "${spaces}'-- checking ${pkg_dep_tmp[$recurse_lev]} for $1"; fi
    if /usr/xpg4/bin/grep -qE "${bAnchor}${1} *${eAnchor}" ${pkg_dep_tmp[$recurse_lev]}
    then
      remove_circ=`mktemp ${TMPDIR}${myname}.${myPID}.${recurse_lev}.circ-dep.XXXXXXX`
      if [[ $verbose -ge 5 ]]; then echo "${spaces}ERROR - Circular dep for $1 found, removing"; fi
      grep -vw $1 ${pkg_dep_tmp[$recurse_lev]} > $remove_circ
      cat $remove_circ > ${pkg_dep_tmp[$recurse_lev]} 
      rm $remove_circ
    fi
  
    while read dep_package
    do
      # recursively get the deps for this dep
      let "recurse_lev += 1" # increment recurse depth to prevent tmp file stomping
      getdeps $dep_package
      if [[ $? -ne 0 ]]
      then
        echo "ERROR - recursive parameter passed was invalid - FAIL!"
        rm ${pkg_dep_tmp[$recurse_lev]}
        return 1
      fi
      let "recurse_lev -= 1" # decrement after recurse
    done <${pkg_dep_tmp[$recurse_lev]} # redirects stdin to file

    # remove indent
    spaces="${spaces%     }"
    
    if [[ $verbose -ge 1 ]]; then echo "${spaces}'-- done checking deps of $1"; fi
    rm ${pkg_dep_tmp[$recurse_lev]}
  fi
  if [[ $verbose -ge 1 ]]; then echo "${spaces}'-- Adding package $1 to dependency list";echo; fi
  echo "$1" >>$pkg_dep_unmet
  return 0
}

#  Function: 
#     getdeps-list ()
#  Purpose:
#     read packages from a file and call getdeps for each package listed in the file
#  Parameters:
#     none
#  Expected Result:
#     - Files altered: no files altered directly, but subsidiary calls to getdeps potentially modify:
#     $listfile, $checked in order to build an ordered dependency list for packages listed
#     in the file containing a list of packages to check for with the -f switch.
#     - The above mentioned list suppliments / is supplimented by any single package specified by the -p switch
#     - Depending on verbosity, messages are output to stdout
#     
#  Return conditions:
#     1 for false.
#      - If there is a problem encounterd by getdeps
#     0 for true
#      - if getdeps() is able to process all packages listed in the file
# 
getdeps-list ()
{
  while read next_package
  do
    if [[ $verbose -ge 1 ]]; then echo; echo "Checking package $next_package read from $listFile"; fi
    getdeps $next_package
    retval=$?
    if [[ ! $retval -ge 0 ]]
    then
      echo "WARNING - error getting dependencies for listed package: $next_package with baselevel ${flags[$BASE_NUM]}."
      return 1
    fi
  done <$listFile
  return 0
}

#  Function: 
#     isSFW()
#  Purpose:
#     Determines if a package is a Sun Free-Ware package (these come on the Companion disk / iso )
#  Parameters:
#     $1 package name to check
#  Expected Result:
#     - Files altered: none
#     - Depending on verbosity, messages are output to stdout
#  Error conditions:
#     1 for false.
#      - If 
#     0 for true
#      - if 
# 
isSFW()
{
  if [ -z "$1" ]
  then
    echo "ERROR - isSFW - parameter passed is zero length FAIL"
    return 3
  else
    if [[ $verbose -ge 5 ]]; then echo "${spaces}checking $1 for SFWness";fi
  fi
  if echo $1 | /usr/xpg4/bin/grep -q "SFW"
  #if echo $1 | grep "SFW"
  then
    if [[ $verbose -ge 5 ]]; then echo "${spaces}'-- is a SFW Package";fi
    return 0
  else
    if [[ $verbose -ge 5 ]]; then echo "${spaces}'-- is NOT a SFW package";fi
    return 1
  fi
}

#  Function: 
#     printdeps()
#  Purpose:
#     Prints to stdin the list of dependencies geterated for packages.
#  Parameters:
#     None
#  Expected Result:
#     A list of packages is output to stdout.  Depending on the mode and verbosity
#     specified, the list will look different.
#  Error conditions:
#     0 for true
#      - when done
# 
printdeps()
{
  # print the contents of the temp file containing the deps
  if [[ $verbose -ge 1 ]]
  then
    echo
    echo "#--------------------------------------------#"
    echo "#    Dependency list has been generated.     #"
    echo "#--------------------------------------------#"
    echo
    if [[ $verbose -ge 3 ]];then echo "$(cat $checked | wc -l) unique dependencies checked"; echo; fi
    if [[ ! -z $package ]] && [[ $listmode -ge 1 ]]
    then
      echo "for package: ----------> ${package}"
      echo "and packages from: ----> $listFile"
      echo "'-- with Soft Group ${flags[$BASE_NUM]}"
      cat $listFile | sed 's/^/                         /'
      echo "'-- you will need to install ( Note: this is an aggregated list ):"
    elif [[ ! -z $package ]]
    then 
      echo "For package: --------> ${package}"
      echo "'-- with Soft Group ${flags[$BASE_NUM]}"
    elif [[ $listmode -ge 1 ]]
    then
      echo "For packages from -----> ${listFile}:"
      echo "'-- with Soft Group ${flags[$BASE_NUM]}"
      cat $listFile | sed -e :a -e '$!N; s/\n/,/;ta'
      echo "'-- you will need to install in this order:"
    fi
    echo
  fi

  if [[ "$scriptSafe" -eq 1 ]]
  then 
    cat $pkg_dep_unmet | sed -e :a -e '$!N; s/\n/,/;ta'
  else
    if [[ $verbose -ge 3 ]];then echo "$(cat $pkg_dep_unmet | wc -l) packages to install:"; echo; fi
    cat $pkg_dep_unmet
  fi
  return 0
}

#  Function: 
#     use-o-matic()
#  Purpose:
#    prints to stdout a usage statement with a brief description of each option
#  Parameters:
#    None
#  Expected Result:
#    Usage statement output to stdout
#  Error conditions:
#     0 for true when done
# 
use-o-matic()
{
  echo
  echo "pkgrez: Utility for resolving the dependencies for Solaris packages on SVR4 systems"
  echo
  echo "Usage: `basename $0` options (-sp)"
  echo
  echo "    -f pkglist  - a file containg a list of packages seperated
                  by newline characters"
  echo "    -g softgrp  - numerical value for the Software Group to 
                  compare against as a base install."
  echo "                  The Default is $BASE_NUM for a level of ${flags[$BASE_NUM]}"
  echo "                  Valid options are:"
  for a in {0..6}
  do
    echo "                    $a to check against a base level of ${flags[$a]}"
  done
  echo "    -m          - Streams output format. Files are listed  across
                  the page, separated by commas."
  echo "    -p package  - the Package to check the dependencies of"
  echo "    -s seedFile - a file containg a seed list of packages 
                   that it is safe to assume that they are installed as well.
                   This is useful if you are building a minimal install and want
                   to build upon the results of prior research into dependency
                   resolution."
  echo "    -v          - turn on verbose output.  Add more for greater 
                  verbosity (right now the features don't increase if you add 
                  more than 5)."
  return 0
}

#############################################
#                                           #
#          MAIN section of pkgrez           #
#                                           #
#############################################

# q&d validate that some options were passed
if [ $# -eq "$NO_ARGS" ]  # Script invoked with no command-line args?
then
  use-o-matic
  exit $E_OPTERROR        # Exit and explain usage, if no argument(s) given.
fi  

# process command line options
while getopts ":f:g:mp:s:Sv" Option
do
  case $Option in
    f ) listFile=$OPTARG 
        ((listmode++)) ;;
    g ) BASE_NUM=$OPTARG ;;
    m ) ((scriptSafe++)) ;;
    p ) package=$OPTARG ;;
    s ) seedFile=$OPTARG ;;
    v ) ((verbose+=1)) ;;
    * ) echo "ERROR bad parameter passed: ${OPTARG}.  Perhaps it needs an argument?"
        use-o-matic
        exit $E_OPTERROR ;;  # Exit and explain usage
  esac
done
shift $(($OPTIND - 1)) # Move argument pointer to next.

# Check if incompatable options Script-Safe and Verbosity are both turned on
if (( scriptSafe )) && (( verbose ))
then
  echo "ERROR - cannot use incompatible options S and v."
  echo "Script-Safe-is is pretty much the opposite of Verbosity."
  use-o-matic
  exit $E_OPTERROR  # Exit and explain usage
fi

# Make sure we were given at least 1 package to check
if [ -z $package ] && [ ! -e "$listFile" ]
then
  echo "ERROR - no package(s) specified"
  use-o-matic
  exit $E_OPTERROR  # Exit and explain usage
fi

# create temp files to keep all the discovered dep names in
pkg_dep_unmet=`mktemp ${TMPDIR}/${myName}.${myPID}.deps_unmet.XXXXXXX`
checked=`mktemp ${TMPDIR}/${myName}.${myPID}.deps_unmet.XXXXXXX`
if [[ ! -e $pkg_dep_unmet ]] || [[ ! -e $checked ]]
then
  echo "File error: couldn't create files for dep checkings" &>2
  return 1
fi

# Verbose header
if [[ $verbose -ge 1 ]]
then
  echo
  echo "Running $0 with PID ${myPID} "
  echo
  if [ ! -z $package ]
  then
    echo "Checking for dependencies of $package"
  else
    echo "List mode: checking dependencies of the packages listed in $listFile"
  fi 
  if [ -e $seedFile ]
  then
    echo "Using seed list from file: $seedFile"
  fi
  echo "The meta-cluser (Software Group) base level to check against is ${flags[$BASE_NUM]}"
  echo "-----------------------------------"
fi

# if we are using a seed file, copy those to the already checked file
if [[ -e $seedFile ]]
then 
  cat $seedFile >> $checked
fi

# if -p was used then first check the package specified
if [ ! -z $package ]
then
  if [[ $verbose -ge 1 ]]; then echo "*** - The minimum meta-cluster containing $package is: `$SPC -r $SPC_REP -P $package | head -n 1` "; fi

  getdeps $package
  retval=$?
  if [[ ! $retval -eq 0 ]]
  then
    echo "WARNING - error getting dependencies for $package with baselevel ${flags[$BASE_NUM]}."
    rm $pkg_dep_unmet
    rm $checked
    exit 1
  fi
fi

# now do any listed packages
if [[ -e $listFile ]]
then
  getdeps-list 
  retval=$?
  if [[ ! $retval -eq 0 ]]
  then
    rm $pkg_dep_unmet
    rm $checked
    echo "WARNING - error getting dependencies for packages from $listFile with baselevel ${flags[$BASE_NUM]}."
    exit 1
  fi
fi

# validate that the files are still fine, just in case
if [[ ! -e $pkg_dep_unmet ]] || [[ ! -e $checked ]]
then
  echo "File error.  Package dependency list file went away prematurely!  Please check your logic Mr. programmer!  Or the stack, or something else went south.  It either is or already been clobberin time!"
  exit 1
fi

# output the list generated, cleanup, and exit
printdeps
rm $pkg_dep_unmet
rm $checked
exit 0
